/**
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.api.adwords.starterkit.Synchronizer;

import com.google.api.adwords.starterkit.db.keys.AdGroupKey;
import com.google.api.adwords.starterkit.db.keys.CampaignKey;
import com.google.api.adwords.starterkit.db.structures.AdGroupStructure;
import com.google.api.adwords.starterkit.db.structures.CampaignStructure;
import com.google.api.adwords.starterkit.db.tables.AdGroupTable;
import com.google.api.adwords.starterkit.db.tables.CampaignTable;
import com.google.api.adwords.starterkit.db.tables.KeywordTable;
import com.google.api.adwords.starterkit.settings.StarterKitSettings;
import com.google.api.adwords.starterkit.utils.AdWordsReportHandler;
import com.google.api.adwords.starterkit.utils.DumpExceptions;
import com.google.api.adwords.starterkit.utils.NotFullyProcessedException;
import com.google.api.adwords.starterkit.utils.RowUtils;
import com.google.api.adwords.starterkit.utils.ServiceUtils;
import com.google.api.adwords.starterkit.utils.Utils;
import com.google.api.adwords.v201101.cm.AdGroup;
import com.google.api.adwords.v201101.cm.AdGroupBids;
import com.google.api.adwords.v201101.cm.AdGroupOperation;
import com.google.api.adwords.v201101.cm.AdGroupReturnValue;
import com.google.api.adwords.v201101.cm.AdGroupServiceInterface;
import com.google.api.adwords.v201101.cm.AdGroupStatus;
import com.google.api.adwords.v201101.cm.ApiError;
import com.google.api.adwords.v201101.cm.ApiException;
import com.google.api.adwords.v201101.cm.Bid;
import com.google.api.adwords.v201101.cm.BudgetOptimizerAdGroupBids;
import com.google.api.adwords.v201101.cm.ConversionOptimizerAdGroupBids;
import com.google.api.adwords.v201101.cm.ManualCPCAdGroupBids;
import com.google.api.adwords.v201101.cm.ManualCPMAdGroupBids;
import com.google.api.adwords.v201101.cm.Money;
import com.google.api.adwords.v201101.cm.Operator;
import com.google.api.adwords.v201101.cm.Predicate;
import com.google.api.adwords.v201101.cm.PredicateOperator;
import com.google.api.adwords.v201101.cm.ReportDefinitionReportType;

import org.apache.log4j.Logger;

import java.rmi.RemoteException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * The Class AdGroupSynchronizer is responsible to keep all AdGroups 
 * information from AdWords system synchronized with local DB.
 * 
 * @author rodrigo.f.vale@gmail.com (Rodrigo Vale)
 */
public class AdGroupSynchronizer {
  private static final Logger log = Logger.getLogger("StarterKit");
  private StarterKitSettings starterKitSetting;
  private ServiceUtils serviceUtils;
  public CampaignTable campaignTable;
  public AdGroupTable adGroupTable;
  public KeywordTable keywordTable;

  public AdGroupSynchronizer(StarterKitSettings starterKitSetting, 
      Connection conn) {
    this.setServiceUtils(new ServiceUtils(starterKitSetting));
    this.starterKitSetting = starterKitSetting;
    
    campaignTable = new CampaignTable(conn);
    adGroupTable = new AdGroupTable(conn);
    keywordTable = new KeywordTable(conn);
  }

  /**
   * Process the updates in adGroups table.
   */
  protected void put() {
    log.debug("Preparing AdGroups to be sent to AdWords");
    
    if (starterKitSetting.getAdGroupSettings().getAutoPause()) {
      adGroupTable.autoPause();
    }
    adGroupTable.grantBanned();
    
    adGroupTable.updateCampaignsIds();

    HashMap<Integer, AdGroupBids> biddingStrategies = campaignTable
        .getBiddingStrategy();

    int tries = 0;
    while (tries < starterKitSetting.getRetriesOnError()) {
      log.info("Updating/Creation AdGroups - Try: " + tries);
      HashSet<AdGroupOperation> operations = new HashSet<AdGroupOperation>();
      try {
        ResultSet rs = adGroupTable.getModified();
        log.debug("Processing Tuples");
        while (rs.next()) {
          AdGroupStructure agStruct = adGroupTable.getResultSet(rs);

          // Create AdGroup
          AdGroup adGroup = new AdGroup();
          adGroup.setName(agStruct.getName());

          AdGroupBids biddingStrategy = biddingStrategies.get(agStruct
              .getLocalCampaignId());
          
          if (biddingStrategy == null) {
            log.error(
                "There is no Bidding Strategy defined for AdGroupLocalId " +
                agStruct.getLocalId());
          } else if (biddingStrategy instanceof ManualCPCAdGroupBids) {
            ManualCPCAdGroupBids bids = (ManualCPCAdGroupBids) biddingStrategy;
            Money money = new Money(null, agStruct.getMaxCpc());
            Bid bid = new Bid(money);
            bids.setKeywordMaxCpc(bid);
            adGroup.setBids(bids);
          } else if (biddingStrategy instanceof ManualCPMAdGroupBids) {
            ManualCPMAdGroupBids bids = (ManualCPMAdGroupBids) biddingStrategy;
            bids.setMaxCpm(new Bid(new Money(null, agStruct.getMaxCpm())));
            adGroup.setBids(bids);
          } else if (biddingStrategy instanceof BudgetOptimizerAdGroupBids) {
            BudgetOptimizerAdGroupBids bids 
                = (BudgetOptimizerAdGroupBids) biddingStrategy;
            adGroup.setBids(bids);
          } else if (biddingStrategy instanceof ConversionOptimizerAdGroupBids) {
            ConversionOptimizerAdGroupBids bids 
                = (ConversionOptimizerAdGroupBids) biddingStrategy;
            bids
                .setTargetCpa(
                new Bid(new Money(null, agStruct.getTargetCpa())));
            adGroup.setBids(bids);
          }
          adGroup.setCampaignId(agStruct.getCampaignId());
          adGroup.setStatus(agStruct.getStatus());

          AdGroupOperation operation = new AdGroupOperation();
          if ((agStruct.getAdGroupId() == 0)
              && (starterKitSetting.getAdGroupSettings().getAutoCreation())) {
            operation.setOperand(adGroup);
            operation.setOperator(Operator.ADD);
          } else {
            adGroup.setId(agStruct.getAdGroupId());
            operation.setOperand(adGroup);
            operation.setOperator(Operator.SET);
          }

          operations.add(operation);
        }
        rs.close();
      } catch (SQLException e3) {
        log.debug("SQL Error on saveAdGroups. Error: " + e3.getMessage());
        log.debug("StarterKit will try to move on. Some updates may be lost.");
      }
      
      try {
        processOperations(operations);
        tries = starterKitSetting.getRetriesOnError();
      } catch (NotFullyProcessedException e) {
        log.warn(e.getMessage());
        tries++;
        Utils.sleep(starterKitSetting.getSleepOnPolicyError());
      }
    }
    adGroupTable.resetStatus();
  }

  /**
   * Save processed updates from AdGroups to AdWords.
   * 
   * @param operations
   *          The updates to be sent to AdWords
   * @throws NotFullyProcessedException 
   */
  protected void processOperations(HashSet<AdGroupOperation> operations) 
      throws NotFullyProcessedException {
    boolean notFullyProcessedException = false;
    AdGroupServiceInterface adGroupService;
    adGroupService = getServiceUtils().getAdGroupService();
    if (operations.size() > 0) {
      log.info("Processing: " + operations.size());
      List<HashSet<AdGroupOperation>> blocks = Utils.split(operations, 1000);
      for (HashSet<AdGroupOperation> opBlocks : blocks) {
        log.info("Processing Block: " + opBlocks.size());
        AdGroupOperation[] op = opBlocks.toArray(new AdGroupOperation[opBlocks
            .size()]);
        // Add ad group
        AdGroupReturnValue result = null;
        try {
          result = adGroupService.mutate(op);
          // Display ad group criteria.
          if ((result != null) && (result.getValue() != null)) {
            for (AdGroup adgroup : result.getValue()) {
              long campaignId = adgroup.getCampaignId();
              int localCampaignId = campaignTable.getLocalId(campaignId);
              long adGroupId = adgroup.getId();
              String name = adgroup.getName();
              AdGroupKey adGroupKey = new AdGroupKey(localCampaignId, name);
              adGroupTable.setBatchId(adGroupKey, adGroupId, false, false);
            }
            adGroupTable.execBatch();
          }
          log.debug("Updated " + op.length + " AdGroups");
        } catch (ApiException e1) {
          // catching all AdWords errors, like Policy errors.
          // These errors will be recorded at StarterKit database and
          // they should be fixed by the application owner, not by the
          // framework.
          notFullyProcessedException = true;
          Pattern pattern = Pattern.compile("operations\\[(\\d+)\\].*");
          for (ApiError apiError : e1.getErrors()) {
            Matcher matcher = pattern.matcher(apiError.getFieldPath());
            log.debug(apiError.getApiErrorType());
            String adGroups = "";
            if (matcher.matches()) {
              int i = Integer.parseInt(matcher.group(1));
              AdGroupOperation adGroupOperation = op[i];
              AdGroup adGroup = adGroupOperation.getOperand();
              long campaignId = adGroup.getCampaignId();
              int localCampaignId = campaignTable.getLocalId(campaignId);
              String name = adGroup.getName();
              AdGroupKey adGroupKey = new AdGroupKey(localCampaignId, name);
              adGroupTable.setBatchUpdated(adGroupKey, false, false);
              adGroups = adGroups.concat("(" + localCampaignId + ")" + name
                  + "[" + apiError.getTrigger() + "], ");
            }
            log.debug(adGroups);
          }
          adGroupTable.execBatch();
        } catch (RemoteException e) {
          notFullyProcessedException = true;
          DumpExceptions.dumpRemoteException(e);
        }
      }
    }
    if (notFullyProcessedException) {
      throw 
          new NotFullyProcessedException("Some operations were not processed");
    }
  }

  /**
   * Based on a AdWords Report. StarterKit will load all Ad Groups informations
   * and store it in a local SQLite db.
   * 
   * @param row
   *          a single row from the AdWords Report
   * @param adGroupIds
   *          a list of processed AdGroups to avoid duplication
   * @param campaingsLocalId
   *          a map to localCampaignId for optimization
   */
  public void processReportData(HashMap<String, String> row,
      HashSet<Long> adGroupIds, HashMap<String, Integer> campaingsLocalId) {

    AdGroupStructure agStruct = new AdGroupStructure();
    agStruct.setAdGroupId(RowUtils.getLong(row, "adGroupID", 0));
    agStruct.setCampaignId(RowUtils.getLong(row, "campaignID", 0));
    agStruct.setName(RowUtils.getString(row, "adGroup", ""));
    String status = RowUtils.getString(row, "adGroupState", "").toUpperCase();
    agStruct.setStatus(AdGroupStatus.fromString(status));
    
    double dDisplayNetMaxCpc 
        = RowUtils.getDouble(row, "displayNetworkMaxCPC",0);
    long displayNetMaxCpc = (long) (dDisplayNetMaxCpc * 1000000);
    agStruct.setMaxContentCpc(displayNetMaxCpc);
    
    agStruct.setMaxCpa(RowUtils.getLong(row, "maxCPA", 0));
    
    double dSiteMaxCPC = RowUtils.getDouble(row, "siteMaxCPC",0);
    long siteMaxCPC = (long) (dSiteMaxCPC * 1000000);
    agStruct.setSiteMaxCpc(siteMaxCPC);
    
    double dDefaultMaxCPC = RowUtils.getDouble(row, "defaultMaxCPC", 0);
    long defaultMaxCPC = (long) (dDefaultMaxCPC * 1000000);
    agStruct.setMaxCpc(defaultMaxCPC);
    
    double dMaxCPM = RowUtils.getDouble(row, "maxCPM", 0);
    long maxCPM = (long) (dMaxCPM * 1000000);
    agStruct.setMaxCpm(maxCPM);
    
    agStruct.setTargetCpa(RowUtils.getLong(row, "targetCPA (1-per-click)", 0));

    if (!agStruct.getStatus().equals("DELETED")) {
      String campaignName = RowUtils.getString(row, "campaign", "");
      String adGroupName = RowUtils.getString(row, "adGroup", "");
      if (!adGroupIds.contains(agStruct.getAdGroupId())) {
        adGroupIds.add(agStruct.getAdGroupId());

        CampaignKey campaignKey = new CampaignKey(campaignName);
        CampaignStructure campaignStructure = campaignTable.get(campaignKey);
        
        if (campaignStructure != null) {
          agStruct.setLocalCampaignId(campaignStructure.getLocalId());

          AdGroupKey adGroupKey = new AdGroupKey(agStruct.getLocalCampaignId(),
              adGroupName);
          AdGroupStructure agStructDB = adGroupTable.get(adGroupKey);

          if (agStructDB == null) {
            // AdGroups created outside of StarterKit can't be modified
            // setting canModify flags to false
            agStruct.setDefaultCpcModificable(false);
            agStruct.setStatusModifiable(false);

            // they can't also be banned
            agStruct.setBanned(false);

            adGroupTable.saveBatch(agStruct, false, false);
          } else {
            agStruct.setLocalId(agStructDB.getLocalId());
            agStruct.setLocalCampaignId(agStructDB.getLocalCampaignId());
            agStruct.setDefaultCpcModificable(agStructDB.isDefaultCpcModificable());
            agStruct.setStatusModifiable(agStructDB.isStatusModifiable());
            if (!agStruct.getStatus().equals(agStructDB.getStatus())) {
              agStruct.setStatusModifiable(false);
            }
            if (agStruct.getMaxCpc() != agStructDB.getMaxCpc()) {
              agStruct.setDefaultCpcModificable(false);
            }
            agStruct.setBanned(false);
            adGroupTable.saveBatch(agStruct, false, false);
          }
        } else {
          log.debug("Ignoring Campaign " + campaignName);
        }
      }
    }
  }
  
  /**
   * Load adgroups from adwords.
   * 
   */
  public void get() {
    HashSet<Long> adGroupIds = new HashSet<Long>();

    log.debug("Start to load AdGroups");

    adGroupTable.resetStatus();

    AdWordsReportHandler adWordsReportHandler = getReportHandler();

    log.debug("Processing AdGroups");
    HashMap<String, Integer> campaingsLocalId = campaignTable
        .getAllCampaignsLocalId();
    while (adWordsReportHandler.hasNext()) {
      HashMap<String, String> row = adWordsReportHandler.next();
      processReportData(row, adGroupIds, campaingsLocalId);
    }
    adGroupTable.execBatch();
    adGroupTable.resetIsUpdated(false);
    adGroupTable.resetIsTouched(false);
  }
  
  /**
   * Load ad groups performance from ad words.
   */
  public void getStats() {
    log.debug("Processing AdGroups Performance JSON Objects");

    adGroupTable.resetStats();
    try {
      ResultSet rs = keywordTable.getStats();
      while (rs.next()) {
        adGroupTable.saveStatsBatch(
            rs.getFloat("CTR"),
            rs.getLong("Clicks"),
            rs.getFloat("Cost"),
            rs.getLong("Impressions"),
            rs.getLong("Conversions"),
            rs.getLong("Transactions"),
            (float) rs.getDouble("averagePosition"),
            (float) rs.getDouble("adGroupMaxCpa"),
            (float) rs.getDouble("firstPageCpc"),
            rs.getLong("AdGroupId"));        
      }      
      rs.close();     
      adGroupTable.execStatsBatch();
    } catch (SQLException e) {
      DumpExceptions.dumpSQLException(e);
    }
  }

  /**
   * Gets the adgroup structure report.
   * 
   * @return the ad group structure report
   */
  public AdWordsReportHandler getReportHandler() {
    log.debug("Defining AdGroup Structure Report");

    String fields[] = new String[] { "CampaignId", "CampaignName", "Id",
        "AdGroupName", "KeywordMaxCpc", "AverageCpc", "AverageCpm",
        "SiteMaxCpc", "Status", "TargetCpa", "CostPerConversion",
        "KeywordContentMaxCpc", "MaxCpm", "PercentCpa" };

    String fileName = starterKitSetting.getCacheDir().concat(
        "AdGroupStrututureReport.xml");

    String campaignStatus[] = null;
    if (starterKitSetting.getDownloadDeletedCampaigns()) {
      campaignStatus = new String[] { "ACTIVE", "PAUSED", "DELETED" };
    } else {
      campaignStatus = new String[] { "ACTIVE", "PAUSED" };
    }
    
    // Create campaing predicate.
    Predicate campaingPredicate = new Predicate();
    campaingPredicate.setField("CampaignStatus");
    campaingPredicate.setOperator(PredicateOperator.IN);
    campaingPredicate.setValues(campaignStatus);

    String adGroupStatus[] = new String[] { "ENABLED", "PAUSED" };    
    
    // Create AdGroup predicate.
    Predicate adGroupPredicate = new Predicate();
    adGroupPredicate.setField("Status");
    adGroupPredicate.setOperator(PredicateOperator.IN);
    adGroupPredicate.setValues(adGroupStatus);
        
    Predicate[] predicates = new Predicate[] { campaingPredicate, 
        adGroupPredicate };

    AdWordsReportHandler adWordsReportHandler = new AdWordsReportHandler(
        getServiceUtils(), starterKitSetting);
    adWordsReportHandler.requestAdWordsReport(
        ReportDefinitionReportType.ADGROUP_PERFORMANCE_REPORT, fields,
        fileName, predicates);

    return adWordsReportHandler;
  }

  public void setServiceUtils(ServiceUtils serviceUtils) {
    this.serviceUtils = serviceUtils;
  }

  public ServiceUtils getServiceUtils() {
    return serviceUtils;
  }
}